2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIContactController.h"
18 #import "AIContactStatusDockOverlaysPlugin.h"
19 #import "AIContentController.h"
20 #import "AIDockController.h"
21 #import "AIInterfaceController.h"
22 #import "AIPreferenceController.h"
23 #import "ESContactAlertsController.h"
24 #import <AIUtilities/AIColorAdditions.h>
25 #import <AIUtilities/AIDictionaryAdditions.h>
26 #import <AIUtilities/AIParagraphStyleAdditions.h>
27 #import <AIUtilities/AIArrayAdditions.h>
28 #import <AIUtilities/ESImageAdditions.h>
29 #import <Adium/AIAbstractListController.h>
30 #import <Adium/AIAccount.h>
31 #import <Adium/AIChat.h>
32 #import <Adium/AIIconState.h>
34 #define SMALLESTRADIUS 15
35 #define RADIUSRANGE 36
36 #define SMALLESTFONTSIZE 14
37 #define FONTSIZERANGE 30
39 #define DOCK_OVERLAY_ALERT_SHORT AILocalizedString(@"Display name in the dock icon",nil)
40 #define DOCK_OVERLAY_ALERT_LONG DOCK_OVERLAY_ALERT_SHORT
42 @interface AIContactStatusDockOverlaysPlugin (PRIVATE)
44 - (NSImage *)overlayImageFlash:(BOOL)flash;
45 - (void)preferencesChanged:(NSNotification *)notification;
46 - (void)flushPreferenceColorCache;
49 @implementation AIContactStatusDockOverlaysPlugin
56 overlayObjectsArray = [[NSMutableArray alloc] init];
59 //Register as a contact observer (For signed on / signed off)
60 [[adium contactController] registerListObjectObserver:self];
62 //Register as a chat observer (for unviewed content)
63 [[adium contentController] registerChatObserver:self];
65 [[adium notificationCenter] addObserver:self
66 selector:@selector(chatClosed:)
71 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
74 image1 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
75 image2 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
77 //Install our contact alert
78 [[adium contactAlertsController] registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
81 - (void)uninstallPlugin
83 [[adium contactController] unregisterListObjectObserver:self];
84 [[adium contentController] unregisterChatObserver:self];
85 [[adium notificationCenter] removeObserver:self];
86 [[adium preferenceController] unregisterPreferenceObserver:self];
90 * @brief Short description
91 * @result A short localized description of the action
93 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
95 return(DOCK_OVERLAY_ALERT_SHORT);
99 * @brief Long description
100 * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
102 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
104 return(DOCK_OVERLAY_ALERT_LONG);
110 - (NSImage *)imageForActionID:(NSString *)actionID
113 return([NSImage imageNamed:@"DockAlert" forClass:[self class]]);
117 * @brief Details pane
118 * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
120 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
126 * @brief Perform an action
128 * @param actionID The ID of the action to perform
129 * @param listObject The listObject associated with the event triggering the action. It may be nil
130 * @param details If set by the details pane when the action was created, the details dictionary for this particular action
131 * @param eventID The eventID which triggered this action
132 * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
134 - (void)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
136 BOOL isMessageEvent = [[adium contactAlertsController] isMessageEvent:eventID];
141 if((chat = [userInfo objectForKey:@"AIChat"]) &&
142 (chat != [[adium interfaceController] activeChat]) &&
143 (![overlayObjectsArray containsObjectIdenticalTo:chat])){
144 [overlayObjectsArray addObject:chat];
146 //Wait until the next run loop so this event is done processing (and our unviewed content count is right)
147 [self performSelector:@selector(_setOverlay)
151 /* The chat observer method is responsible for removing this overlay later */
154 }else if(listObject){
155 NSTimer *removeTimer;
157 //Clear any current timer for this object o ahve its overlay removed
158 if(removeTimer = [listObject statusObjectForKey:@"DockOverlayRemoveTimer"]) [removeTimer invalidate];
160 //Add a timer to remove this overlay
161 removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
163 selector:@selector(removeDockOverlay:)
166 [listObject setStatusObject:removeTimer
167 forKey:@"DockOverlayRemoveTimer"
170 if(![overlayObjectsArray containsObject:listObject]){
171 [overlayObjectsArray addObject:listObject];
174 //Wait until the next run loop so this event is done processing
175 [self performSelector:@selector(_setOverlay)
181 - (void)removeDockOverlay:(NSTimer *)removeTimer
183 AIListObject *inObject = [removeTimer userInfo];
185 [overlayObjectsArray removeObjectIdenticalTo:inObject];
187 [inObject setStatusObject:nil
188 forKey:@"DockOverlayRemoveTimer"
194 - (void)chatClosed:(NSNotification *)notification
196 AIChat *chat = [notification object];
198 [overlayObjectsArray removeObjectIdenticalTo:chat];
204 * @brief Allow multiple actions?
206 * If this method returns YES, every one of this action associated with the triggering event will be executed.
207 * If this method returns NO, only the first will be.
209 * Don't allow multiple dock actions to occur. While a series of "Bounce every 5 seconds," "Bounce every 10 seconds,"
210 * and so on actions could be combined sanely, a series of "Bounce once" would make the dock go crazy.
212 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
217 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
218 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
220 //Grab colors from status coloring plugin's prefs
221 [self flushPreferenceColorCache];
222 signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
223 signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
224 unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
226 backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
227 backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
228 backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
231 - (void)flushPreferenceColorCache
233 [signedOffColor release]; signedOffColor = nil;
234 [signedOnColor release]; signedOnColor = nil;
235 [unviewedContentColor release]; unviewedContentColor = nil;
236 [backSignedOffColor release]; backSignedOffColor = nil;
237 [backSignedOnColor release]; backSignedOnColor = nil;
238 [backUnviewedContentColor release]; backUnviewedContentColor = nil;
241 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
243 if([inObject isKindOfClass:[AIAccount class]]){
244 //When an account signs on or off, force an overlay update as it may have silently changed
246 if([inModifiedKeys containsObject:@"Online"]){
247 NSEnumerator *enumerator = [[[overlayObjectsArray copy] autorelease] objectEnumerator];
248 AIListObject *listObject;
249 BOOL madeChanges = NO;
251 while(listObject = [enumerator nextObject]){
252 if(([listObject respondsToSelector:@selector(account)]) &&
253 ([(id)listObject account] == inObject) &&
254 ([overlayObjectsArray containsObjectIdenticalTo:listObject])){
255 [overlayObjectsArray removeObject:listObject];
260 if(madeChanges) [self _setOverlay];
268 * @brief When a chat no longer has unviewed content, remove it from display
270 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
272 if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]){
274 if(![inChat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]){
275 if([overlayObjectsArray containsObjectIdenticalTo:inChat]){
276 [overlayObjectsArray removeObjectIdenticalTo:inChat];
288 //Remove & release the current overlay state
290 [[adium dockController] removeIconStateNamed:@"ContactStatusOverlay"];
291 [overlayState release]; overlayState = nil;
294 //Create & set the new overlay state
295 if([overlayObjectsArray count] != 0){
297 overlayState = [[AIIconState alloc] initWithImages:[NSArray arrayWithObjects:[self overlayImageFlash:NO], [self overlayImageFlash:YES], nil]
301 [[adium dockController] setIconState:overlayState named:@"ContactStatusOverlay"];
306 - (NSImage *)overlayImageFlash:(BOOL)flash
308 NSEnumerator *enumerator;
309 ESObjectWithStatus *object;
311 NSParagraphStyle *paragraphStyle;
315 NSImage *image = (flash ? image1 : image2);
317 //Pre-calc some sizes
318 #warning 1 - [[adium dockController] dockIconScale] is the wrong relationship?
319 dockIconScale = 1- [[adium dockController] dockIconScale];
320 iconHeight = (SMALLESTRADIUS + (RADIUSRANGE * dockIconScale));
323 bottom = top - iconHeight;
325 //Set up the string details
326 font = [NSFont boldSystemFontOfSize:(SMALLESTFONTSIZE + (FONTSIZERANGE * dockIconScale))];
327 paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
331 [[NSColor clearColor] set];
332 NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
334 //Draw overlays for each contact
335 enumerator = [overlayObjectsArray reverseObjectEnumerator];
336 while((object = [enumerator nextObject]) && !(top < 0) && bottom < 128){
337 float left, right, arcRadius, stringInset;
339 NSColor *backColor = nil, *textColor = nil, *borderColor = nil;
341 //Create the pill frame
342 arcRadius = (iconHeight / 2.0f);
343 stringInset = (iconHeight / 4.0f);
344 left = 1 + arcRadius;
345 right = 127 - arcRadius;
347 path = [NSBezierPath bezierPath];
348 [path setLineWidth:((iconHeight/2.0) * 0.13333f)];
350 [path moveToPoint: NSMakePoint(left, top)];
351 [path lineToPoint: NSMakePoint(right, top)];
354 [path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius)
359 [path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
360 [path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius)
367 [path moveToPoint: NSMakePoint(right, bottom)];
368 [path lineToPoint: NSMakePoint(left, bottom)];
371 [path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
376 [path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
377 [path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius) radius:arcRadius startAngle:180 endAngle:90 clockwise:YES];
381 if(!([contact integerStatusObjectForKey:KEY_UNVIEWED_CONTENT] && flash)){
382 backColor = [[contact displayArrayForKey:@"Label Color"] averageColor];
383 textColor = [[contact displayArrayForKey:@"Text Color"] averageColor];
387 if([object integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]){ //Unviewed
389 backColor = [NSColor whiteColor];
390 textColor = [NSColor blackColor];
392 backColor = backUnviewedContentColor;
393 textColor = unviewedContentColor;
395 }else if([object integerStatusObjectForKey:@"Signed On"]){ //Signed on
396 backColor = backSignedOnColor;
397 textColor = signedOnColor;
399 }else if([object integerStatusObjectForKey:@"Signed Off"]){ //Signed off
400 backColor = backSignedOffColor;
401 textColor = signedOffColor;
406 backColor = [NSColor whiteColor];
409 textColor = [NSColor blackColor];
412 //Lighten/Darken the back color slightly
413 if([backColor colorIsDark]){
414 backColor = [backColor darkenBy:-0.15];
415 borderColor = [backColor darkenBy:-0.3];
417 backColor = [backColor darkenBy:0.15];
418 borderColor = [backColor darkenBy:0.3];
427 //Get the object's display name
428 [[object displayName] drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)
429 withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
431 nameString = [[[NSAttributedString alloc] initWithString:[contact displayName] attributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]] autorelease];
432 [nameString drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)];*/
434 //Move down to the next pill
435 top -= (iconHeight + 7.0 * dockIconScale);
436 bottom = top - iconHeight;